#include "DeferredRenderer.h"
#include "..\Managers\GameManager.h"
#include "..\GameObjects\Player.h"
#include <math.h>

DeferredRenderer::DeferredRenderer(const Vec2i &viewport_size)
	: viewport_size(viewport_size), renderTarget_size(viewport_size*4)
{
	//G-Buffer
	diffuses.resize(renderTarget_size.x()*renderTarget_size.y());
	normals.resize(renderTarget_size.x()*renderTarget_size.y());
	speculars.resize(renderTarget_size.x()*renderTarget_size.y());
	depths.resize(renderTarget_size.x()*renderTarget_size.y());
	shininess.resize(renderTarget_size.x()*renderTarget_size.y());
	
	//Misc info
	symbols.resize(renderTarget_size.x()*renderTarget_size.y());
	map_info.resize(renderTarget_size.x()*renderTarget_size.y());

	//Light Buffer
	lights.resize(renderTarget_size.x()*renderTarget_size.y());
	lightMap.resize(renderTarget_size.x()*renderTarget_size.y());

	ambient.bkgr = Vec3f(0.992f, 0.992f, 0.992f);
	ambient.frgr = Vec3f(0.993f, 0.993f, 0.993f);

	renderTarget = std::make_shared<TCODImage>(renderTarget_size.x(), renderTarget_size.y());
}

DeferredRenderer::~DeferredRenderer()
{
}

void DeferredRenderer::shade(const Vec2i &pos, const Vec3f &data, RenderLayer layer, RenderTarget target, BlendType blending)
{
	Vec2i viewspace_pos;
	int index;
	if(toViewSpace(pos, viewspace_pos, index) == false)
		return;

	RenderData *renderData;
	switch(target)
	{
	case RT_DIFFUSE:	renderData = &diffuses[index];	break;
	case RT_NORMAL:		renderData = &normals[index];	break;
	case RT_SPECULAR:	renderData = &speculars[index];	break;
	default: break;
	};

	Vec3f *layer_data;
	switch(layer)
	{
	case RL_BACKGROUND: layer_data = &renderData->bkgr; break;
	case RL_FOREGROUND: layer_data = &renderData->frgr; break;
	};

	switch(blending)
	{
	case BLEND_DEFAULT:
	case BLEND_NONE:
	case BLEND_SET:			(*layer_data) = data; break;
	case BLEND_MULTIPLY:	(*layer_data) *= data; break;
	case BLEND_LIGHTEN:		(*layer_data) = layer_data->max(data); break;
	case BLEND_DARKEN:		(*layer_data) = layer_data->min(data); break;
	case BLEND_SCREEN:		(*layer_data) = layer_data->screen(data); break;
	case BLEND_COLOR_DODGE:	(*layer_data) = layer_data->colorDodge(data); break;
	case BLEND_COLOR_BURN:	(*layer_data) = layer_data->colorBurn(data); break;
	case BLEND_ADD:			(*layer_data) = layer_data->add(data); break;
	case BLEND_BURN:		(*layer_data) = layer_data->burn(data); break;
	case BLEND_OVERLAY:		(*layer_data) = layer_data->overlay(data); break;
	default:				(*layer_data) = data; break;
	};
}

void DeferredRenderer::shade(const Vec2i &pos, float data, RenderTarget target)
{
	Vec2i viewspace_pos;
	int index;
	if(toViewSpace(pos, viewspace_pos, index) == false)
		return;

	float *renderData;
	switch(target)
	{
	case RT_DEPTH:		renderData = &depths[index];	break;
	case RT_SHININESS:	renderData = &shininess[index];	break;
	default: break;
	};

	(*renderData) = data;
}

void DeferredRenderer::light(const Vec2i &pos, float h, const TCODMapPtr &fov, const Vec3f &color, int radius, BlendType blending, bool isPlayerLightSource)
{
	Vec2i viewspace_pos;
	int index;
	if(toViewSpace(pos, viewspace_pos, index) == false)
		return;

	auto &lightData = lights[index];

	lightData.light_fov = fov;

	lightData.pos.x() = viewspace_pos.x();
	lightData.pos.y() = h;
	lightData.pos.z() = viewspace_pos.y();
	lightData.color = color;
	lightData.radius = radius;
	lightData.blending = blending;
	lightData.isPlayerLightSource = isPlayerLightSource;
}

void DeferredRenderer::addSymbol(const Vec2i &pos, uint8 character, float weight, bool hide_if_hidden)
{
	Vec2i viewspace_pos;
	int index;
	if(toViewSpace(pos, viewspace_pos, index) == false)
		return;

	if(symbols[index].weight < weight)
	{
		symbols[index].weight = weight;
		symbols[index].character = character;
		symbols[index].hide_if_hidden = hide_if_hidden;
	}
}

void DeferredRenderer::setMapInfo(const Vec2i &pos, bool transparent, bool walkable)
{
	Vec2i viewspace_pos;
	int index;
	if(toViewSpace(pos, viewspace_pos, index) == false)
		return;

	map_info[index].transparent = transparent;
	map_info[index].walkable = walkable;
}

void DeferredRenderer::prepare()
{
	for(auto &diffuse : diffuses)
		diffuse = RenderData();

	for(auto &normal : normals)
		normal = RenderData();

	for(auto &specular : speculars)
		specular = RenderData();

	for(auto &depth : depths)
		depth = 0.0f;
	
	for(auto &shine : shininess)
		shine = 0.0f;
	
	for(auto &symbol : symbols)
		symbol = Symbol();

	for(auto &info : map_info)
		info = MapData();

	for(auto &light : lights)
		light = LightData();
	
	for(auto &light : lightMap)
		light = LightMapData();
}

void DeferredRenderer::render()
{
	PlayerPtr player = GameManager::getSingleton()->getPlayer();
	TCODMapPtr player_fov = player->getFOV();
	std::vector<LightData*> temp_lights;
	
	//First we update the fov maps with fresh data
	for(unsigned int y = 0; y < renderTarget_size.y(); y++)
	{
		for(unsigned int x = 0; x < renderTarget_size.x(); x++)
		{
			auto index = x+y*renderTarget_size.x();
			player_fov->setProperties(x,y, map_info[index].transparent, map_info[index].walkable);
			if(lights[index].light_fov)
			{
				if(lights[index].isPlayerLightSource == false)
					lights[index].light_fov->setProperties(x,y, map_info[index].transparent, map_info[index].walkable);
				
				temp_lights.push_back(&lights[index]);
			}
		}
	}
	player_fov->computeFov(renderTarget_size.x()/2, renderTarget_size.y()/2, player->getSightRadius(), true, FOV_DIAMOND);
	computeLightMap(temp_lights);

	//Finally, we use all the information available to compute the final coloring and write to the map console viewport
	auto console = GameManager::getSingleton()->getMapConsole();
	for(unsigned int y = 0; y < renderTarget_size.y(); y++)
	{
		for(unsigned int x = 0; x < renderTarget_size.x(); x++)
		{
			bool is_hidden = false;
			float hidden = 0.0f;
			//If tile is hidden from player's view, don't spend time shading it
			if(player_fov->isInFov(x,y) == false)
			{
				/*console->setCharBackground(x, y, TCODColor::black);
				console->putChar(x, y, ' ', TCOD_BKGND_NONE);
				console->setCharForeground(x,y, TCODColor::black);
				continue;*/
				hidden = 0.2f;
				is_hidden = true;
			}

			auto index = x+y*renderTarget_size.x();
			auto &lightMapData = lightMap[index];
			float NdotL_sum = 0.0f;
			for(float value : lightMapData.NdotL)
				NdotL_sum += value;
			float term_sum = 0.0f;
			for(float value : lightMapData.term)
				term_sum += value;

			//Compute for background
			{
				auto &diffuseColor = diffuses[index].bkgr;
				auto &specularColor = speculars[index].bkgr;
				Vec3f color_out =	(diffuseColor * lightMapData.attenuationColor * NdotL_sum * (1.0f-hidden)) +	//Diffuse color
									(specularColor * lightMapData.attenuationColor * term_sum * (1.0f-hidden)) +	//Specular color
									(diffuseColor * ambient.bkgr);									//Ambient color
				color_out.clamp(0.0f, 1.0f);
				color_out *= 255.0f;
				TCODColor color_out_tcod((int)color_out.x(), (int)color_out.y(), (int)color_out.z());
				//console->setCharBackground(x, y, color_out_tcod);
				renderTarget->putPixel(x,y,color_out_tcod);
			}
			//Compute for foreground
			if(is_hidden == false || symbols[index].hide_if_hidden == false)
			{
				auto &diffuseColor = diffuses[index].frgr;
				auto &specularColor = speculars[index].frgr;
				auto &symbol = symbols[index].character;
				Vec3f color_out =	(diffuseColor * lightMapData.attenuationColor * NdotL_sum) +	//Diffuse color
									(specularColor * lightMapData.attenuationColor * term_sum) +	//Specular color
									(diffuseColor * ambient.frgr);									//Ambient color
				color_out.clamp(0.0f, 1.0f);
				color_out *= 255.0f;
				TCODColor color_out_tcod((int)color_out.x(), (int)color_out.y(), (int)color_out.z());
				/*console->putChar(x/2, y/2, symbol, TCOD_BKGND_NONE);
				console->setCharForeground(x/2, y/2, color_out_tcod);*/
			}
			else
			{
				//console->putChar(x/2, y/2, ' ', TCOD_BKGND_NONE);
			}
		}
	}
	renderTarget->save("test.png");
	renderTarget->blit2x(console.get(), 0,0);
}

void DeferredRenderer::computeLightMap(std::vector<LightData*> &lightPos)
{
	float cutoff = 0.001f;

	int count = 0;
	//Start iterating over the lights and generate new lightmap data
	for(auto light : lightPos)
	{
		if(light->isPlayerLightSource == false)
			light->light_fov->computeFov((int)light->pos.x(), (int)light->pos.z(), light->radius, true, FOV_BASIC);

		//Compute how lights shade the map, effectively building a LightShadingBuffer
		for(unsigned int y = 0; y < renderTarget_size.y(); y++)
		{
			for(unsigned int x = 0; x < renderTarget_size.x(); x++)
			{
				float shadow = 0.0f;
				if(light->light_fov->isInFov(x,y) == false)
					shadow = 0.2f;

				count++;
				auto index = x+y*renderTarget_size.x();
				auto &normal = normals[index];
				auto depth = depths[index];
				Vec3f tile_pos(x, depth, y);

				if((int)tile_pos.x() == (int)light->pos.x() &&
					(int)tile_pos.z() == (int)light->pos.z())
				{
					int hello = 0;
				}

				// calculate normalized light vector and distance to sphere light surface
				Vec3f L = light->pos - tile_pos;
				float distance = L.length();
				float d = (distance - light->radius) > 0.0f ? (distance - light->radius) : 0.0f;
				L.normalize();

				// calculate basic attenuation
				float denom = d/light->radius + 1.0f;
				float attenuation = 1.0f / (denom*denom);

				// scale and bias attenuation such that:
				// - attenuation == 0 at extent of max influence
				// - attenuation == 1 when d == 0
				attenuation = (attenuation - cutoff) / (1.0f - cutoff);
				attenuation = attenuation > 0.0f ? attenuation : 0.0f;

				// cosine angle incidence
				float NdotL = normal.bkgr.dot(L);
				//NdotL = abs(NdotL);
				NdotL = NdotL > 0.0f ? NdotL : 0.0f;

				float term = computeTerm(TERM_GAUSS, normal.bkgr, L, Vec3f(0.0f, -1.0f, 0.0f), NdotL, 0.5f);

				//Store values in our lightmap buffer
				Vec3f r_attenuationColor = light->color * attenuation;
				Vec3f &l_attenuationColor = lightMap[index].attenuationColor;
				switch(light->blending)
				{
				case BLEND_DEFAULT:
				case BLEND_NONE:
				case BLEND_SET:			l_attenuationColor = r_attenuationColor; break;
				case BLEND_MULTIPLY:	l_attenuationColor *= r_attenuationColor; break;
				case BLEND_LIGHTEN:		l_attenuationColor = l_attenuationColor.max(r_attenuationColor); break;
				case BLEND_DARKEN:		l_attenuationColor = l_attenuationColor.min(r_attenuationColor); break;
				case BLEND_SCREEN:		l_attenuationColor = l_attenuationColor.screen(r_attenuationColor); break;
				case BLEND_COLOR_DODGE:	l_attenuationColor = l_attenuationColor.colorDodge(r_attenuationColor); break;
				case BLEND_COLOR_BURN:	l_attenuationColor = l_attenuationColor.colorBurn(r_attenuationColor); break;
				case BLEND_ADD:			l_attenuationColor = l_attenuationColor.add(r_attenuationColor); break;
				case BLEND_BURN:		l_attenuationColor = l_attenuationColor.burn(r_attenuationColor); break;
				case BLEND_OVERLAY:		l_attenuationColor = l_attenuationColor.overlay(r_attenuationColor); break;
				default:				l_attenuationColor = r_attenuationColor; break;
				};
				l_attenuationColor *= (1.0f - shadow);
				
				lightMap[index].NdotL.push_back(NdotL);
				lightMap[index].term.push_back(term);
			}
		}
	}
}

float DeferredRenderer::computeTerm(TermType term, const Vec3f &N, const Vec3f &L, const Vec3f &viewDirection, float NdotL, float shininess)
{
	float temp;
	switch(term)
	{
	case TERM_PHONG:
		{
			////Phong term
			Vec3f reflectDir = N.reflect(L.inverted());
			temp = Vec3f::clampf(viewDirection.dot(reflectDir), 0.0f, 1.0f);
			temp = NdotL != 0.0f ? temp : 0.0f;
			temp = powf(temp, shininess);
		} break;
	case TERM_BLINN:
		{
			//Blinn term
			Vec3f halfAngle = (L + viewDirection).normalize();
			temp = N.dot(halfAngle);
			temp = Vec3f::clampf(temp, 0.0f, 1.0f);
			temp = NdotL != 0.0f ? temp : 0.0f;
			temp = powf(temp, shininess);
		} break;
	case TERM_GAUSS:
		{
			//Gaussian term
			Vec3f halfAngle = (L + viewDirection).normalize();
			float angleNormalHalf = acosf(N.dot(halfAngle));
			float exponent = angleNormalHalf / shininess;
			exponent = -(exponent * exponent);
			temp = exp(exponent);
			temp = NdotL != 0.0 ? temp : 0.0;
		} break;
	};
	return temp;
}

bool DeferredRenderer::toViewSpace(const Vec2i &in_worldspace, Vec2i &out_viewspace, int &index)
{
	auto lower_bounds = GameManager::getSingleton()->getPlayer()->getLowerBounds();

	out_viewspace = in_worldspace - lower_bounds;
	if(out_viewspace.x() >= renderTarget_size.x() || out_viewspace.y() >= renderTarget_size.y())
		return false;

	index = out_viewspace.x()+out_viewspace.y()*renderTarget_size.x();
	if(index < 0)
		return false;

	return true;
}
